/*
 * Unidata Platform Copyright (c) 2013-2020, UNIDATA LLC, All rights reserved.
 *
 * Commercial License This version of Unidata Platform is licensed commercially
 * and is the appropriate option for the vast majority of use cases.
 *
 * Please see the Unidata Licensing page at:
 * https://unidata-platform.com/license/ For clarification or additional
 * options, please contact: info@unidata-platform.com ------- Disclaimer:
 * ------- THIS SOFTWARE IS DISTRIBUTED "AS-IS" WITHOUT ANY WARRANTIES,
 * CONDITIONS AND REPRESENTATIONS WHETHER EXPRESS OR IMPLIED, INCLUDING WITHOUT
 * LIMITATION THE IMPLIED WARRANTIES AND CONDITIONS OF MERCHANTABILITY,
 * MERCHANTABLE QUALITY, FITNESS FOR A PARTICULAR PURPOSE, DURABILITY,
 * NON-INFRINGEMENT, PERFORMANCE AND THOSE ARISING BY STATUTE OR FROM CUSTOM OR
 * USAGE OF TRADE OR COURSE OF DEALING.
 */
package org.unidata.mdm.dq.core.util;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.OffsetDateTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;

import org.apache.commons.lang3.StringUtils;
import org.jgrapht.graph.DirectedMultigraph;
import org.unidata.mdm.core.type.data.ArrayAttribute;
import org.unidata.mdm.core.type.data.ArrayAttribute.ArrayDataType;
import org.unidata.mdm.core.type.data.SimpleAttribute;
import org.unidata.mdm.core.type.data.impl.AbstractArrayAttribute;
import org.unidata.mdm.core.type.data.impl.BooleanSimpleAttributeImpl;
import org.unidata.mdm.core.type.data.impl.DateSimpleAttributeImpl;
import org.unidata.mdm.core.type.data.impl.IntegerSimpleAttributeImpl;
import org.unidata.mdm.core.type.data.impl.NumberSimpleAttributeImpl;
import org.unidata.mdm.core.type.data.impl.StringSimpleAttributeImpl;
import org.unidata.mdm.core.type.data.impl.TimeSimpleAttributeImpl;
import org.unidata.mdm.core.type.data.impl.TimestampSimpleAttributeImpl;
import org.unidata.mdm.core.type.upath.UPathApplicationMode;
import org.unidata.mdm.core.type.upath.UPathExecutionContext;
import org.unidata.mdm.core.util.SecurityUtils;
import org.unidata.mdm.dq.core.exception.DataQualityExceptionIds;
import org.unidata.mdm.dq.core.exception.DataQualityRuntimeException;
import org.unidata.mdm.dq.core.service.impl.function.java.JavaCleanseFunctionLibrary;
import org.unidata.mdm.dq.core.type.cleanse.CleanseFunctionExecutionScope;
import org.unidata.mdm.dq.core.type.cleanse.CleanseFunctionInputParam;
import org.unidata.mdm.dq.core.type.cleanse.CleanseFunctionOutputParam;
import org.unidata.mdm.dq.core.type.cleanse.CleanseFunctionParam;
import org.unidata.mdm.dq.core.type.cleanse.CleanseFunctionPortFilteringMode;
import org.unidata.mdm.dq.core.type.model.source.CleanseFunctionGroup;
import org.unidata.mdm.dq.core.type.model.source.CompositeCleanseFunctionNode;
import org.unidata.mdm.dq.core.type.model.source.CompositeCleanseFunctionSource;
import org.unidata.mdm.dq.core.type.model.source.CompositeCleanseFunctionTransition;
import org.unidata.mdm.dq.core.type.model.source.DataQualityModel;
import org.unidata.mdm.dq.core.type.model.source.JavaCleanseFunctionSource;
import org.unidata.mdm.dq.core.type.model.source.constant.ArrayValueConstant;
import org.unidata.mdm.dq.core.type.model.source.constant.CleanseFunctionConstant;
import org.unidata.mdm.dq.core.type.model.source.constant.SingleValueConstant;

/**
 * The Class.
 *
 * @author ilya.bykov
 */
public final class DQUtils {
    /**
     * System function classes.
     */
    private static final String[] SYSTEM_FUNCTIONS = {
            "org.unidata.mdm.dq.core.service.impl.function.system.convert.ParseBoolean",
            "org.unidata.mdm.dq.core.service.impl.function.system.convert.ParseDate",
            "org.unidata.mdm.dq.core.service.impl.function.system.convert.ParseInteger",
            "org.unidata.mdm.dq.core.service.impl.function.system.convert.ParseNumber",
            "org.unidata.mdm.dq.core.service.impl.function.system.logic.And",
            "org.unidata.mdm.dq.core.service.impl.function.system.logic.Empty",
            "org.unidata.mdm.dq.core.service.impl.function.system.logic.Not",
            "org.unidata.mdm.dq.core.service.impl.function.system.logic.Or",
            "org.unidata.mdm.dq.core.service.impl.function.system.logic.Xor",
            "org.unidata.mdm.dq.core.service.impl.function.system.math.IntegerRange",
            "org.unidata.mdm.dq.core.service.impl.function.system.math.NumberRange",
            "org.unidata.mdm.dq.core.service.impl.function.system.math.Divide",
            "org.unidata.mdm.dq.core.service.impl.function.system.math.Multiply",
            "org.unidata.mdm.dq.core.service.impl.function.system.math.Sum",
            "org.unidata.mdm.dq.core.service.impl.function.system.math.Subtract",
            "org.unidata.mdm.dq.core.service.impl.function.system.math.Max",
            "org.unidata.mdm.dq.core.service.impl.function.system.math.Min",
            "org.unidata.mdm.dq.core.service.impl.function.system.math.Round",
            "org.unidata.mdm.dq.core.service.impl.function.system.misc.CheckINN",
            "org.unidata.mdm.dq.core.service.impl.function.system.misc.CheckExists",
            "org.unidata.mdm.dq.core.service.impl.function.system.misc.CheckValue",
            "org.unidata.mdm.dq.core.service.impl.function.system.string.CheckLength",
            "org.unidata.mdm.dq.core.service.impl.function.system.string.CheckMask",
            "org.unidata.mdm.dq.core.service.impl.function.system.string.CleanupNoise",
            "org.unidata.mdm.dq.core.service.impl.function.system.string.CompressWhitespaces",
            "org.unidata.mdm.dq.core.service.impl.function.system.string.Concatenate",
            "org.unidata.mdm.dq.core.service.impl.function.system.string.DefaultValue",
            "org.unidata.mdm.dq.core.service.impl.function.system.string.FormatString",
            "org.unidata.mdm.dq.core.service.impl.function.system.string.PadLeft",
            "org.unidata.mdm.dq.core.service.impl.function.system.string.PadRight",
            "org.unidata.mdm.dq.core.service.impl.function.system.string.RegEx",
            "org.unidata.mdm.dq.core.service.impl.function.system.string.Substring",
            "org.unidata.mdm.dq.core.service.impl.function.system.string.LowerCase",
            "org.unidata.mdm.dq.core.service.impl.function.system.string.TitleCase",
            "org.unidata.mdm.dq.core.service.impl.function.system.string.UpperCase",
            "org.unidata.mdm.dq.core.service.impl.function.system.string.Trim" };
    /**
     * System category.
     */
    public static final String CATEGORY_SYSTEM = "SYSTEM";
    /**
     * Undefined category.
     */
    public static final String CATEGORY_UNDEFINED = "UNDEFINED";
    /**
     * Default model instace id.
     */
    public static final String DEFAULT_MODEL_INSTANCE_ID = "default";
    /**
     * Analogously to entities, the root group name.
     */
    public static final String ROOT_FUNCTION_GROUP_NAME = "ROOT";
    /**
     * Instantiates a new DQ utils.
     */
    private DQUtils() {
        super();
    }
    /**
     * Sonar-ready access to SF array.
     *
     * @return list of system cleanse function binary names
     */
    public static String[] systemFunctions() {
        return SYSTEM_FUNCTIONS;
    }
    /**
     * Creates default model layout (system groups and functions).
     * @param storageId the storage id
     * @return default basic model source
     */
    public static DataQualityModel defaultModel(String storageId) {

        String user = SecurityUtils.getCurrentUserName();
        OffsetDateTime now = OffsetDateTime.now();

        CleanseFunctionGroup system = new CleanseFunctionGroup()
                .withCreateDate(now)
                .withCreatedBy(user)
                .withName("SYSTEM")
                .withDisplayName("app.dq.functions.group.system.name")
                .withDescription("app.dq.functions.group.system.description");

        CleanseFunctionGroup convert = new CleanseFunctionGroup()
                .withCreateDate(now)
                .withCreatedBy(user)
                .withName("CONVERT")
                .withDisplayName("app.dq.functions.group.convert.name")
                .withDescription("app.dq.functions.group.convert.description");

        CleanseFunctionGroup logic = new CleanseFunctionGroup()
                .withCreateDate(now)
                .withCreatedBy(user)
                .withName("LOGIC")
                .withDisplayName("app.dq.functions.group.logic.name")
                .withDescription("app.dq.functions.group.logic.description");

        CleanseFunctionGroup math = new CleanseFunctionGroup()
                .withCreateDate(now)
                .withCreatedBy(user)
                .withName("MATH")
                .withDisplayName("app.dq.functions.group.math.name")
                .withDescription("app.dq.functions.group.math.description");

        CleanseFunctionGroup misc = new CleanseFunctionGroup()
                .withCreateDate(now)
                .withCreatedBy(user)
                .withName("MISC")
                .withDisplayName("app.dq.functions.group.misc.name")
                .withDescription("app.dq.functions.group.misc.description");

        CleanseFunctionGroup string = new CleanseFunctionGroup()
                .withCreateDate(now)
                .withCreatedBy(user)
                .withName("STRING")
                .withDisplayName("app.dq.functions.group.string.name")
                .withDescription("app.dq.functions.group.string.description");

        CleanseFunctionGroup root = new CleanseFunctionGroup()
                .withCreateDate(now)
                .withCreatedBy(user)
                .withName(ROOT_FUNCTION_GROUP_NAME)
                .withDisplayName("app.dq.functions.group.root.name")
                .withDescription("app.dq.functions.group.root.description")
                .withGroups(system.withGroups(convert, logic, math, misc, string));

        List<JavaCleanseFunctionSource> sources = new ArrayList<>(SYSTEM_FUNCTIONS.length);
        for (String name : SYSTEM_FUNCTIONS) {

            JavaCleanseFunctionSource jcfs = new JavaCleanseFunctionSource().withCreateDate(now).withCreatedBy(user)
                    .withLibraryName(JavaCleanseFunctionLibrary.DQ_SYSTEM_CLEANSE_FUNCTIONS_LIBRARY)
                    .withLibraryVersion(JavaCleanseFunctionLibrary.DQ_SYSTEM_CLEANSE_FUNCTIONS_VERSION)
                    .withJavaClass(name).withName(name.substring(name.lastIndexOf('.') + 1));

            if (name.contains(".convert.")) {
                jcfs.withGroupName("ROOT.SYSTEM.CONVERT");
            } else if (name.contains(".logic.")) {
                jcfs.withGroupName("ROOT.SYSTEM.LOGIC");
            } else if (name.contains(".math.")) {
                jcfs.withGroupName("ROOT.SYSTEM.MATH");
            } else if (name.contains(".misc.")) {
                jcfs.withGroupName("ROOT.SYSTEM.MISC");
            } else if (name.contains(".string.")) {
                jcfs.withGroupName("ROOT.SYSTEM.STRING");
            } else {
                throw new DataQualityRuntimeException("Cannot order function [{}]. Unknown category.",
                        DataQualityExceptionIds.EX_DQ_SYSTEM_CLEANSE_FUNCTION_NOT_ORDERED, name);
            }

            sources.add(jcfs);
        }

        return new DataQualityModel().withVersion(0).withStorageId(storageId).withCreateDate(now).withCreatedBy(user)
                .withJavaFunctions(sources).withFunctionGroup(root);
    }
    /*
     * Converts single value.
     */
    @SuppressWarnings("unchecked")
    private static CleanseFunctionParam ofSingleConstantValue(SingleValueConstant<?> cvd, String name, boolean input) {

        if (cvd.getType() == null) {
            return null;
        }

        SimpleAttribute<?> attribute = null;
        switch (cvd.getType()) {
            case BOOLEAN:
                attribute = new BooleanSimpleAttributeImpl(name, ((SingleValueConstant<Boolean>) cvd).getValue());
                break;
            case DATE:
                attribute = new DateSimpleAttributeImpl(name, ((SingleValueConstant<LocalDate>) cvd).getValue());
                break;
            case INTEGER:
                attribute = new IntegerSimpleAttributeImpl(name, ((SingleValueConstant<Long>) cvd).getValue());
                break;
            case NUMBER:
                attribute = new NumberSimpleAttributeImpl(name, ((SingleValueConstant<Double>) cvd).getValue());
                break;
            case STRING:
                attribute = new StringSimpleAttributeImpl(name, ((SingleValueConstant<String>) cvd).getValue());
                break;
            case TIME:
                attribute = new TimeSimpleAttributeImpl(name, ((SingleValueConstant<LocalTime>) cvd).getValue());
                break;
            case TIMESTAMP:
                attribute = new TimestampSimpleAttributeImpl(name, ((SingleValueConstant<LocalDateTime>) cvd).getValue());
                break;
            default:
                break;
        }

        if (Objects.isNull(attribute)) {
            return null;
        }

        return input
                ? CleanseFunctionInputParam.of(name, attribute)
                : CleanseFunctionOutputParam.of(name, attribute);
    }
    /*
     * Converts array value.
     */
    @SuppressWarnings("unchecked")
    private static CleanseFunctionParam ofArrayConstantValue(ArrayValueConstant<?> cvd, String name, boolean input) {

        if (cvd.getType() == null) {
            return null;
        }

        ArrayAttribute<?> attribute = null;
        switch (cvd.getType()) {
            case DATE:
                attribute = AbstractArrayAttribute.of(ArrayDataType.DATE, name, ((ArrayValueConstant<LocalDate>) cvd).getValues());
                break;
            case INTEGER:
                attribute = AbstractArrayAttribute.of(ArrayDataType.INTEGER, name, ((ArrayValueConstant<Long>) cvd).getValues());
                break;
            case NUMBER:
                attribute = AbstractArrayAttribute.of(ArrayDataType.NUMBER, name, ((ArrayValueConstant<Double>) cvd).getValues());
                break;
            case STRING:
                attribute = AbstractArrayAttribute.of(ArrayDataType.STRING, name, ((ArrayValueConstant<String>) cvd).getValues());
                break;
            case TIME:
                attribute = AbstractArrayAttribute.of(ArrayDataType.TIME, name, ((ArrayValueConstant<LocalTime>) cvd).getValues());
                break;
            case TIMESTAMP:
                attribute = AbstractArrayAttribute.of(ArrayDataType.TIMESTAMP, name, ((ArrayValueConstant<LocalDateTime>) cvd).getValues());
                break;
            default:
                break;
        }

        if (Objects.isNull(attribute)) {
            return null;
        }

        return input
                ? CleanseFunctionInputParam.of(name, attribute)
                : CleanseFunctionOutputParam.of(name, attribute);
    }

    /*
     * Converts constant value.
     */
    @SuppressWarnings("unchecked")
    public static<X extends CleanseFunctionParam> X ofConstant(CleanseFunctionConstant constant, String name, boolean input) {

        if (Objects.isNull(constant) || StringUtils.isBlank(name)) {
            return null;
        }

        if (constant.isSingle()) {
            return (X) ofSingleConstantValue(constant.getSingle(), name, input);
        }

        return (X) ofArrayConstantValue(constant.getArray(), name, input);
    }

    public static UPathExecutionContext ofContext(CleanseFunctionExecutionScope ctx) {

        if (Objects.isNull(ctx)) {
            return null;
        }

        switch (ctx) {
        case GLOBAL:
            return UPathExecutionContext.FULL_TREE;
        case LOCAL:
            return UPathExecutionContext.SUB_TREE;
        default:
            break;
        }

        return null;
    }

    public static UPathApplicationMode ofMode(CleanseFunctionPortFilteringMode mode) {

        if (Objects.isNull(mode)) {
            return null;
        }

        switch (mode) {
        case MODE_ALL:
            return UPathApplicationMode.MODE_ALL;
        case MODE_ONCE:
            return UPathApplicationMode.MODE_ONCE;
        case MODE_ALL_WITH_INCOMPLETE:
            return UPathApplicationMode.MODE_ALL_WITH_INCOMPLETE;
        default:
            break;
        }

        return null;
    }

    public static DirectedMultigraph<CompositeCleanseFunctionNode, CompositeCleanseFunctionTransition>
        ofSource(CompositeCleanseFunctionSource ccfs) {

        DirectedMultigraph<CompositeCleanseFunctionNode, CompositeCleanseFunctionTransition> graph
            = new DirectedMultigraph<>(CompositeCleanseFunctionTransition.class);

        Map<Integer, CompositeCleanseFunctionNode> nodes = new HashMap<>();
        for (CompositeCleanseFunctionNode n : ccfs.getLogic().getNodes()) {
            graph.addVertex(n);
            nodes.put(n.getNodeId(), n);
        }

        for (CompositeCleanseFunctionTransition t : ccfs.getLogic().getTransitions()) {
            CompositeCleanseFunctionNode from = nodes.get(t.getFromNodeId());
            CompositeCleanseFunctionNode to = nodes.get(t.getToNodeId());
            graph.addEdge(from, to, t);
        }

        return graph;
    }
}
